package com.qingzhuge.manager.security.config;

import com.qingzhuge.autoconfigure.QingTingProperties;
import com.qingzhuge.manager.security.filter.JsonAuthenticationFilter;
import com.qingzhuge.manager.security.filter.JwtAuthorizationTokenFilter;
import com.qingzhuge.manager.security.handler.*;
import com.qingzhuge.manager.security.service.DefinedFilterInvocationSecurityMetadataSource;
import com.qingzhuge.manager.security.service.RedisTokenRepositoryImpl;
import com.qingzhuge.manager.security.service.UserDetailsServiceImpl;
import com.qingzhuge.manager.security.support.MobileCodeAuthenticationProvider;
import com.qingzhuge.manager.security.support.UrlRoleVoter;
import com.qingzhuge.manager.security.support.UsernamePasswordAuthenticationProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.security.access.vote.AuthenticatedVoter;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.access.vote.UnanimousBased;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.expression.WebExpressionVoter;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

import javax.annotation.PostConstruct;
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import java.util.ArrayList;
import java.util.List;

/**
 * @author zeroxiao
 * @date 2019/9/11 11:07
 * JSR-250注解:
 * <per>
 * {@link DenyAll} @DenyAll 拒绝所有访问
 * {@link RolesAllowed} @RolesAllowed({"USER", "ADMIN"})  该方法只要具有"USER", "ADMIN"任意一种权限就可以访问。这里可以省 略前缀ROLE_，实际的权限可能是ROLE_ADMIN
 * {@link PermitAll} @PermitAll 允许所有访问
 * </per>
 * prePostEnabled注解:
 * <per>
 * {@link PreAuthorize} @PreAuthorize：在方法执行之前执行，而且这里可以调用方法的参数，也可以得到参数值，这是利用JAVA8的参数名反射特性，如果没用JAVA8，那么也可以利用Spring Security的@P标注参数，或者Spring Data的@Param标注参数。
 * <code>
 * @ @PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)")
 * void changePassword(@P("userId") long userId ){  }
 * </code>
 * 这里表示在changePassword方法执行之前，判断方法参数userId的值是否等于principal中保存的当前用户的userId，或者当前用户是否具有ROLE_ADMIN权限，两种符合其一，就可以访问该方法。
 * {@link PostAuthorize} @PostAuthorize：在方法执行之后执行，而且这里可以调用方法的返回值，如果EL为false，那么该方法也已经执行完了，可能会回滚。EL变量returnObject表示返回的对象。
 * <code>
 * @ @PostAuthorize User getUser("returnObject.userId == authentication.principal.userId or hasPermission(returnObject, 'ADMIN')");
 * </code>
 * {@link PreFilter} @PreFilter：在方法执行之前执行，而且这里可以调用方法的参数，然后对参数值进行过滤或处理或修改，EL变量filterObject表示参数，如有多个参数，使用filterTarget注解参数。只有方法参数是集合或数组才行。（很少会用到，与分页技术不兼容）
 * filterObject对象引用的是传入或传出的List中的某一个元素。
 * </per>
 * securedEnabled注解:
 * {@link Secured} @Secured 在方法上指定安全性要求，只有对应角色的用户才可以调用这些方法。
 * 在方法上(也可以在类或接口上)添加注解将相应地限制对该方法的访问。Spring Security的本地注解支持为该方法定义了一组属性。
 * 这些信息将传递给访问决策管理器（AccessDecisionManager ），以便它做出实际的决定
 *
 * <code>
//    public class SecurityService {
//     * 需要拥有ADMIN或USER权限才能访问，
//     * ADMIN缺陷的话返回所有数据，
//     * USER权限的话只返回和登录用户相同用户名的数据
//     * @return
//
//    @PreAuthorize("hasAnyRole({'ROLE_ADMIN','ROLE_USER'})")
//    @PostFilter("hasRole('ROLE_ADMIN') || filterObject.username == principal.username")
//    public List<User> test1(){
//            User u1 = new User("xuexiaoqiang");
//            User u2 = new User("admin");
//            User u3 = new User("xueqiang");
//            List<User> list = new ArrayList<User>();
//            list.add(u1);
//            list.add(u2);
//            list.add(u3);
//            return list;
//            }
//
//
//     * 需要拥有ADMIN或USER权限才能访问，
//     * 拥有ADMIN权限的用户会将数据全部传入方法中
//     * 拥有USER权限的用户只会将相同用户名的数据传进去
//    @PreAuthorize("hasAnyRole({'ROLE_ADMIN','ROLE_USER'})")
//    @PreFilter("hasRole('ROLE_ADMIN') || "filterObject.username == principal.username")
//    public void test2(List<User> users){
//            for(User u : users){
//            System.out.println(u.getUsername());
//            }
//            }
//            }
 * </code>
 *
 */
@Slf4j
@Configuration
@EnableRedisHttpSession
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired(required = false)
    private List<IWebSecurityConfigurer> webSecurityConfigurers;

    @Autowired
    private ResourceAuthUnauthorizedHandler unauthorizedHandler;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private QingTingProperties properties;

    @Autowired
    private UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider;

    @Autowired
    private MobileCodeAuthenticationProvider mobileCodeAuthenticationProvider;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @PostConstruct
    public void init(){
        if (null != webSecurityConfigurers) {
            AnnotationAwareOrderComparator.sort(webSecurityConfigurers);
        }
    }


    @Bean
    public GrantedAuthorityDefaults grantedAuthorityDefaults() {
        // Remove the ROLE_ prefix
        return new GrantedAuthorityDefaults("");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        //BCrypt格式
        // $2a$14$8P.uUqQYoB0.ivT.exavCemuBh.T4UqBI0zP233ziVAvzugVh9kaO
        // 1 ～3 位文字	$2a	bcrypt版本号
        // 4 ～6 位文字	$14	hash计算的反复回数 2^14=16384回
        // 7 ～29 位文字	$8P.uUqQYoB0.ivT.exavCe	salt值
        // 30 ～最后 位	muBh.T4UqBI0zP233ziVAvzugVh9kaO	密码的本体
        return new BCryptPasswordEncoder(14);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .authenticationProvider(mobileCodeAuthenticationProvider)
                .authenticationProvider(usernamePasswordAuthenticationProvider)
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
        if (null != webSecurityConfigurers){
            webSecurityConfigurers.forEach(action-> {
                try {
                    action.configure(auth);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

    }

    @Bean
    public LoginSuccessEventHandler loginSuccessEventHandler() {
        return new LoginSuccessEventHandler();
    }

    @Bean
    public LoginFailureEvenHandler loginFailureEvenHandler() {
        return new LoginFailureEvenHandler();
    }

    @Bean
    public LogoutSuccessHandler logoutSuccessHandler() {
        return new LogoutSuccessHandler();
    }

    @Bean
    public PersistentTokenRepository redisTokenRepositoryImpl() {
        return new RedisTokenRepositoryImpl();
    }

    /**权限验证数据源*/
    @Bean
    public FilterInvocationSecurityMetadataSource definedFilterInvocationSecurityMetadataSource() {
        return new DefinedFilterInvocationSecurityMetadataSource();
    }

    /**
     * 决策管理
     */
    private AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
        decisionVoters.add(new WebExpressionVoter());
        decisionVoters.add(new AuthenticatedVoter());
        RoleVoter role = new RoleVoter();
        role.setRolePrefix("");
        decisionVoters.add(role);
        decisionVoters.add(new UrlRoleVoter());
        return new UnanimousBased(decisionVoters);
    }

    @Bean
    public AccessDeniedHandler definedAccessDeniedHandler() {
        return new DefinedAccessDeniedHandler();
    }

    @Bean
    public CsrfTokenRepository csrfTokenRepository(){
        return new HttpSessionCsrfTokenRepository();
    }
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
        //OPTIONS请求全部放行
                .antMatchers(HttpMethod.OPTIONS, "/**")
                .antMatchers(HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js"
                )
                .antMatchers(HttpMethod.GET, "/favicon.ico")
                .antMatchers(HttpMethod.GET, "/csrf")
                .antMatchers(HttpMethod.GET, "/static/**")
                // swagger
                .antMatchers(HttpMethod.GET, "/doc.html")
                .antMatchers(HttpMethod.GET, "/swagger**")
                .antMatchers(HttpMethod.GET, "/swagger**/**")
                .antMatchers(HttpMethod.GET, "/webjars/**")
                .antMatchers(HttpMethod.GET, "/*/api-docs")
                // 支付宝回调
                .antMatchers("/api/aliPay/return")
                .antMatchers("/api/aliPay/notify")
                // druid
                .antMatchers("/druid/**");
        if (null != webSecurityConfigurers){
            webSecurityConfigurers.forEach(action-> {
                try {
                    action.configure(web);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用 CSRF
        http
                .csrf()
                .requireCsrfProtectionMatcher(request->"POST".equalsIgnoreCase(request.getMethod()))
                .csrfTokenRepository(csrfTokenRepository())
                .disable();

        // 授权异常
        http
                .exceptionHandling()
                .accessDeniedHandler(definedAccessDeniedHandler())
                .authenticationEntryPoint(unauthorizedHandler);
        //权限验证数据源
        http
                .authorizeRequests()
                .accessDecisionManager(accessDecisionManager())
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
                        fsi.setAccessDecisionManager(accessDecisionManager());
                        fsi.setSecurityMetadataSource(definedFilterInvocationSecurityMetadataSource());
                        return fsi;
                    }
                });
        // 过滤请求
        http
                .authorizeRequests()
                //OPTIONS请求全部放行
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers(HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js"
                ).anonymous()
                .antMatchers(HttpMethod.GET, "/favicon.ico").permitAll()
                .antMatchers(HttpMethod.GET, "/csrf").permitAll()
                .antMatchers(HttpMethod.GET, "/static/**").permitAll()
                // swagger
                .antMatchers(HttpMethod.GET, "/doc.html").permitAll()
                .antMatchers(HttpMethod.GET, "/swagger**").permitAll()
                .antMatchers(HttpMethod.GET, "/swagger**/**").permitAll()
                .antMatchers(HttpMethod.GET, "/webjars/**").permitAll()
                .antMatchers(HttpMethod.GET, "/*/api-docs").permitAll()
                // 支付宝回调
                .antMatchers("/api/aliPay/return").permitAll()
                .antMatchers("/api/aliPay/notify").permitAll()
                // druid
                .antMatchers("/druid/**").permitAll();
        //菜单动态认证
        configureAuthenticated(http);
        //其他接口全部需要认证
        http.authorizeRequests().anyRequest().authenticated();
        //登录
        http
                .formLogin()
                .loginPage(properties.getSecurity().getLoginUrl())
                .loginProcessingUrl(properties.getSecurity().getLoginUrl())
                .failureUrl(properties.getSecurity().getFailureUrl())
                .failureHandler(loginFailureEvenHandler())
                .successHandler(loginSuccessEventHandler())
                .permitAll();
        //登出
        http
                .logout()
                .logoutUrl(properties.getSecurity().getLogoutUrl())
                .logoutSuccessHandler(logoutSuccessHandler())
                .logoutSuccessUrl(properties.getSecurity().getLoginUrl())
                .invalidateHttpSession(true)
                .addLogoutHandler(new SecurityContextLogoutHandler())
                .permitAll();
        //自动登录
        http
                .rememberMe()
                .rememberMeParameter("rememberMe")
                .tokenRepository(redisTokenRepositoryImpl());
        // 防止iframe 造成跨域
        http.headers().frameOptions().disable();
        //开启jwt验证过滤器登录
        if (properties.getSecurity().isOpenJwt()) {
            //使用自定义的 Token过滤器 验证请求的Token是否合法
            http.addFilterBefore(jwtAuthorizationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
            // 不创建会话,因为使用JWT，所以不需要HttpSession
            /*
             * ALWAYS:总是创建HttpSession
             * IF_REQUIRED:Spring Security只会在需要时创建一个HttpSession
             * NEVER:Spring Security不会创建HttpSession，但如果它已经存在，将可以使用HttpSession
             * STATELESS:Spring Security永远不会创建HttpSession，它不会使用HttpSession来获取SecurityContext
             */
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
        http.addFilterAt(jsonAuthenticationFilter(), JsonAuthenticationFilter.class);
        http.headers().cacheControl();
        if (null != webSecurityConfigurers){
            webSecurityConfigurers.forEach(action-> {
                try {
                    action.configure(http);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }

    /**菜单动态认证*/
    private void configureAuthenticated(HttpSecurity http) throws Exception {
        //跳过认证
        String[] exclude = properties.getSecurity().getExcludeUrl().toArray(new String[0]);
        http.authorizeRequests().antMatchers(exclude).permitAll();
        //需要认证
        String[] auth = properties.getSecurity().getAuthUrl().toArray(new String[0]);
        http.authorizeRequests().antMatchers(auth).authenticated();
    }

    /**
     * 注册自定义的UsernamePasswordAuthenticationFilter
     */
    @Bean
    public JsonAuthenticationFilter jsonAuthenticationFilter() throws Exception {
        JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
        filter.setAuthenticationSuccessHandler(loginSuccessEventHandler());
        filter.setAuthenticationFailureHandler(loginFailureEvenHandler());
        filter.setFilterProcessesUrl(properties.getSecurity().getLoginUrl());

        //这句很关键，重用WebSecurityConfigurerAdapter配置的AuthenticationManager，不然要自己组装AuthenticationManager
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }

    @Bean
    public JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter() {
        return new JwtAuthorizationTokenFilter();
    }

}